#include "HIRCode.h"
#include "HXD1.h"

// Control which `HIRCode` is being dumped
const HIRCode *dump_ircode;

// string representing an error
static const char * const Error = "**Error**";

static const char *HIRInst_name[] = {
	[unsigned(HIROper::RET)] = "ret",
	[unsigned(HIROper::BRc)] = "br",
	[unsigned(HIROper::BRu)] = "br",
	[unsigned(HIROper::FNEG)] = "fneg",
	[unsigned(HIROper::ADD)] = "add",
	[unsigned(HIROper::FADD)] = "fadd",
	[unsigned(HIROper::SUB)] = "sub",
	[unsigned(HIROper::FSUB)] = "fsub",
	[unsigned(HIROper::MUL)] = "mul",
	[unsigned(HIROper::FMUL)] = "fmul",
	[unsigned(HIROper::UDIV)] = "udiv",
	[unsigned(HIROper::SDIV)] = "sdiv",
	[unsigned(HIROper::FDIV)] = "fdiv",
	[unsigned(HIROper::UREM)] = "urem",
	[unsigned(HIROper::SREM)] = "srem",
	[unsigned(HIROper::SHL)] = "shl",
	[unsigned(HIROper::LSHR)] = "lshr",
	[unsigned(HIROper::ASHR)] = "ashr",
	[unsigned(HIROper::AND)] = "and",
	[unsigned(HIROper::OR)] = "or",
	[unsigned(HIROper::XOR)] = "xor",
	[unsigned(HIROper::ALLOCA)] = "alloca",
	[unsigned(HIROper::LOAD)] = "load",
	[unsigned(HIROper::STORE)] = "store",
	[unsigned(HIROper::ICMP)] = "icmp",
	[unsigned(HIROper::FCMP)] = "fcmp",
	[unsigned(HIROper::PHI)] = "phi",
	[unsigned(HIROper::CALL)] = "call",
	//[unsigned(HIROper::LABEL)] = "label"
};

static const char *ICMPCond_name[] = {
	[unsigned(ICMPCond::EQ)] = "eq",
	[unsigned(ICMPCond::NE)] = "ne",
	[unsigned(ICMPCond::SGT)] = "sgt",
	[unsigned(ICMPCond::SGE)] = "sge",
	[unsigned(ICMPCond::SLT)] = "slt",
	[unsigned(ICMPCond::SLE)] = "sle"
};

static const char *FCMPCond_name[] = {
	[unsigned(FCMPCond::OEQ)] = "oeq",
	[unsigned(FCMPCond::ONE)] = "one",
	[unsigned(FCMPCond::OGT)] = "ogt",
	[unsigned(FCMPCond::OGE)] = "oge",
	[unsigned(FCMPCond::OLT)] = "olt",
	[unsigned(FCMPCond::OLE)] = "ole"
};

static const char *HIRBtype_name[] = {
	// print `**Error**` for UNKNOWN to show an error
	[unsigned(HIRBtype::UNKNOWN)] = Error,
	[unsigned(HIRBtype::VOID)] = "void",
	[unsigned(HIRBtype::I32)] = "i32",
	[unsigned(HIRBtype::I8)] = "i8",
	[unsigned(HIRBtype::FLOAT)] = "float",
	[unsigned(HIRBtype::DOUBLE)] = "double",
	[unsigned(HIRBtype::POINTER)] = "ptr"
};

// print an imm with type `type` (only for `I8`, `I32`, `FLOAT` and `DOUBLE`)
static void dump_imm(std::ostream & os, const HIRImm &imm, HIRBtype type)
{
	if (type == HIRBtype::I8)
		os << (imm.ival != 0l ? '1' : '0');
	else if (type == HIRBtype::I32)
	{
		if (opts.debug_level != 0)
		{	// print integer in decimal
			os << (int32_t)(imm.ival);
		}
		else
		{
			// print integer in hex
			os << std::hex;
			os << "u0x" << (uint32_t)(imm.ival);
			os << std::dec;
		}
	}
	else if (type == HIRBtype::FLOAT)
	{
		if (opts.debug_level != 0)
			os << imm.fval;
		else
		{
			// print floating point number in hex
			const uint32_t *p = (const uint32_t*)(&imm.fval);
			os << std::hex;
			os << "0x" << *p;
			os << std::dec;
		}
	}
	else if (type == HIRBtype::DOUBLE)
	{
		if (opts.debug_level != 0)
			os << imm.dval;
		else
		{
			os << std::hex;
			os << "0x" << imm.ival;
			os << std::dec;
		}
	}
	else
		os << Error << "immtype" << int(type);
		/*throw std::logic_error(
			std::string("cannot dump invalid type ") + std::to_string(int(type))
		); */
}

/*
 * Print an operand that can be`IMM` with type `immtype`, `TEMP` or `XVAR`.
 * If `not_xvar`, an error flag will be printed for `XVAR` operand.
 * If `immtype` is not specified, it will be `VOID` as default, which will
 * cause an error flag in `dump_imm()` for `IMM` operand.
 * So, it can be used to ban `IMM` operand.
 */
static void dump_opnd(std::ostream & os, const HIROpnd & opnd,
	HIRBtype immtype = HIRBtype::VOID, bool not_xvar = false)
{
	if (opnd.tp == HIROpnd::IMM)
		dump_imm(os, opnd.imm, immtype);
	else if (opnd.tp == HIROpnd::TEMP)
	{
		if (opnd.temp != INVALID_TEMP_NO)
			os << '%' << opnd.temp;
		else
			os << '%' << Error;
	}
	else if (opnd.tp == HIROpnd::XVAR)
	{
		if (opnd.xvaridx >= dump_ircode->extvars().size())
			os << '@' << Error << opnd.xvaridx;
		else
		{
			os << '@';
			if (not_xvar)
				os << Error;
			os << dump_ircode->extvar(opnd.xvaridx).id;
		}
	}
	else
		os << Error << "opndtype" << int(opnd.tp);
}

// print an instruction
static std::ostream & operator<<(std::ostream & os, const HIRInst &inst)
{
	if (inst.oper == HIROper::EMPTY_LINE)
		goto inst_end;

	if (inst.oper != HIROper::LABEL)
	{
		os << '\t';
		if (inst.dest != INVALID_TEMP_NO)
			os << '%' << inst.dest << " = ";
		os << HIRInst_name[unsigned(inst.oper)] << ' ';

		if (inst.oper == HIROper::ALLOCA)
		{
			// print complete type such as `[5 x i32]` for `alloca`
			if (inst.type.length == 1u)
				os << HIRBtype_name[unsigned(inst.type.base_type)];
			else
				os << '[' << inst.type.length << " x " << HIRBtype_name[unsigned(inst.type.base_type)] << ']';
			os << ", align " << inst.alignment;
			goto inst_end;
		}
		else if (inst.oper == HIROper::BRc || inst.oper == HIROper::BRu)
		{
			if (inst.oper == HIROper::BRu)
				os << "label %" << inst.brdest;
			else
				os << "i1 %" << inst.cond << ", label %" << inst.iftrue
					<< ", label %" << inst.iffalse;
			goto inst_end;
		}
		else
		{	// for other instructions

			// for `icmp` and `fcmp`, print the `icond` or `fcond`
			if (inst.oper == HIROper::ICMP)
				os << ICMPCond_name[unsigned(inst.icond)] << ' ';
			else if (inst.oper == HIROper::FCMP)
				os << FCMPCond_name[unsigned(inst.fcond)] << ' ';

			if (inst.type.length != 1u)
				os << Error << inst.type.length;
			os << HIRBtype_name[unsigned(inst.type.base_type)];
			if (inst.oper != HIROper::LOAD)
				// `load` does not have space after type
				os << ' ';
		}
	}
	else
	{	// for label
		if (inst.labelno != INVALID_LABEL)
			os << inst.labelno;
		else
			os << Error;
		os << ':';
		goto inst_end;
	}
	/*
	 * Now `LABEL`, `BRc`, `BRu`, `ALLOCA` have been done,
	 * and continue for other instructions.
	 */

	switch (inst.oper)
	{
	case HIROper::RET:
		if (inst.type.base_type != HIRBtype::VOID)
			dump_opnd(os, inst.value, inst.type.base_type, true);
		break;
	case HIROper::FNEG:
		dump_opnd(os, inst.op1, inst.type.base_type, true);
		break;
	// normal binary operations
	case HIROper::ADD: case HIROper::FADD: case HIROper::SUB: case HIROper::FSUB:
	case HIROper::MUL: case HIROper::FMUL: case HIROper::UDIV: case HIROper::SDIV:
	case HIROper::FDIV: case HIROper::UREM: case HIROper::SREM:
	case HIROper::SHL: case HIROper::LSHR: case HIROper::ASHR:
	case HIROper::AND: case HIROper::OR: case HIROper::XOR:
	case HIROper::ICMP: case HIROper::FCMP:
	{
		HIRBtype op2_btype;
		// only `ADD` is allowed for `XVAR` `op1`
		dump_opnd(os, inst.op1, inst.type.base_type, inst.oper != HIROper::ADD);
		os << ", ";
		// allow pointer added by integer
		if (inst.type.base_type == HIRBtype::POINTER && inst.oper == HIROper::ADD)
			op2_btype = HIRBtype::I32;
		else
			op2_btype = inst.type.base_type;
		dump_opnd(os, inst.op2, op2_btype, false);
	}
		break;
	case HIROper::LOAD:
		os << ", ptr ";
		// `IMM` `inst.pointer` will cause an error flag
		dump_opnd(os, inst.pointer);
		break;
	case HIROper::STORE:
		dump_opnd(os, inst.value, inst.type.base_type, true);
		os << ", ptr ";
		dump_opnd(os, inst.pointer);
		break;
	//case HIROper::ICMP: case HIROper::FCMP:
		// merged in normal binary operations
		break;
	case HIROper::PHI:
		os << "[ ";
		dump_opnd(os, inst.val0, inst.type.base_type, true);
		os << ", %" << inst.label0 << " ], [ ";
		dump_opnd(os, inst.val1, inst.type.base_type, true);
		os << ", %" << inst.label1 << " ]";
		break;
	case HIROper::CALL:
	{
		os << '@';
		// check the indexes
		if (inst.funcidx >= dump_ircode->functs().size())
			os << Error << inst.funcidx;
		else
			os << dump_ircode->funct(inst.funcidx).id;
		os << '(';
		bool first_arg = true;
		if (inst.pfargs != nullptr)
			for (auto & farg : *inst.pfargs)
			{
				HIRBtype atype = farg.type;	// arg type, can be pointer
				if (!first_arg)
					os << ", ";
				first_arg = false;
				os << HIRBtype_name[unsigned(atype)] << ' ';
				dump_opnd(os, farg.arg, atype);
			}
		os << ')';
	}
		break;
	default:
		assert(0);	// Oops!
		break;
	}
inst_end:
	if (inst.cominfo != nullptr)
		os << "\t; " << *inst.cominfo;
	os << std::endl;
	return os;
}

// print an external variable with initialization data
std::ostream & operator<<(std::ostream & os, const extvar_t &xvar)
{
	os << '@';
	// size of every element
	uint32_t bsize = hirsize(xvar.btype);
	//uint32_t alignment;
	// used to print init data
	HIRImm aele;

	aele.ival = 0ll;

	os << xvar.id << " = internal global ";
	if (xvar.is_array)
	{	// array
		os << '[' << xvar.length << " x " << HIRBtype_name[unsigned(xvar.btype)] << "] ";
		if (xvar.data != nullptr)
		{
			const int8_t *pdata = (const int8_t *)xvar.data;
			os << '[';
			for (size_t i = 0ul; i < xvar.length; ++i)
			{
				if (i > 0ul)
					os << ", ";
				os << HIRBtype_name[unsigned(xvar.btype)] << ' ';
				elecpy(&aele.ival, pdata, xvar.btype);
				dump_imm(os, aele, xvar.btype);
				pdata += bsize;
			}
			os << ']';
		}
		else
			os << "zeroinitializer";
		// Temporarily set alignment of array to 16 B
		//alignment = 16u;
	}
	else
	{	// scalar
		os << HIRBtype_name[unsigned(xvar.btype)] << ' ';
		if (xvar.data != nullptr)
		{
			elecpy(&aele.ival, xvar.data, xvar.btype);
			dump_imm(os, aele, xvar.btype);
		}
		else
			os << "zeroinitializer";
		// Alignment is at least the size of it
		//alignment = bsize > 4u ? bsize : 4u;
	}
	os << ", align " << xvar.alignment;
	os << std::endl;
	return os;
}

// print IR code of a function, including its head
std::ostream & operator<<(std::ostream & os, const HIRFunct &funct)
{
	os << "define external ";
	os << HIRBtype_name[unsigned(funct.rettype)] << " @";
	os << funct.id << '(';
	// print parameters
	HSymbolTable *psymtb =
		functable.func(functable.search_func(funct.id)).psymtb;
	size_t nparams = //functable.func(funct.hfteidx).paramtype.size();
		funct.nparams;

	// `psymtb` being `nullptr` means that it is a builtin function
	for (size_t i = 0ul; i < nparams; ++i)
	{
		if (i > 0ul)
			os << ", ";
		if (funct.temps.at(i).type != HIRBtype::POINTER)
		{	// normal param
			os << HIRBtype_name[
				unsigned(funct.temps.at(i).type)
			] << " %";
			if (psymtb != nullptr && psymtb->symbol(i).latn != i
				/*
					* When optimziation is enabled, arguments will not be
					* stored to stack and `latn` of any parameter will be invalid.
					*/
				&& opts.opt_level > 0 && psymtb->symbol(i).latn != INVALID_TEMP_NO)
				os << Error << psymtb->symbol(i).latn;
			else
				os << i;
		}
		else
		{	// array param
			os << HIRBtype_name[
				unsigned(HIRBtype::POINTER)
			] << " %";
			if (psymtb != nullptr && psymtb->symbol(i).pttn != i)
				os << Error << psymtb->symbol(i).pttn;
			else
				os << i;
		}
	}
	os << ") {" << std::endl;
	// print instructions
	for (auto & inst : funct.ircode)
		os << inst;
	if (opts.debug_level != 0 && psymtb != nullptr)
	{
		size_t i;
		/*
		 * If debug mode is enabled, print all temps with
		 * there `fassigned` and `lrefed` for non-builtin functions.
		 * NOTE: if it is not assigned or referenced, `-1` will be printed.
		 * NOTE: only when GRA is enabled will `spillcost` be calculated and printed.
		 */
		os << "; Below are all the temporaries with their"
			" `fassigned`, `frefed`, `lrefed`, `localof` and `spillcost`:"
			<< std::endl;
		os << "; Note: `fassigned`, `frefed`, and `lrefed` will not be "
			"maintained after BBs are divided!" << std::endl;
		const std::vector<HIRTemp> & temps = funct.temps;
		for (i = 0ul; i < temps.size(); ++i)
		{
			auto temp = temps.begin() + i;
			os << ";\t%" << i << ":\t" << HIRBtype_name[unsigned(temp->type)]
				<< '\t';
			// After optimizations are done, some temps or labels may be deleted!
			os << int(temp->fassigned);
			os << '\t' << int(temp->frefed) <<
				'\t' << int(temp->lrefed);
			if (temp->type != HIRBtype::VOID)
				os << '\t' << int(temp->localof);
			else if (temp->localof != INVALID_BB_IDX)
				// label must have invalid `localof`
				os << '\t' << Error << temp->localof;
			if (opts.opt_gra)
				// Spill cost
				os << '\t' << temp->spillcost;
			os << std::endl;
		}
		if (opts.opt_gra)
		{	// Print all loops
			os << "; Below are all loops:" << std::endl;
			i = 0ul;
			for (auto loop = funct.loops.begin(); loop != funct.loops.end(); ++loop, ++i)
			{
				os << ";\tLOOP" << i << ": ";
				for (auto bb : *loop)
					os << bb << ", ";
				os << std::endl;
			}
		}
		// Print all basic blocks
		os << "; Below are all basic blocks with their `begin`, `end`, `succ`:"
			<< std::endl;
		i = 0ul;
		for (auto bb = funct.basic_blocks.begin();
			bb != funct.basic_blocks.end(); ++bb, ++i)
		{
			os << ";\t#" << i << ":\t";
			os << int32_t(bb->begin) << '\t' << int32_t(bb->end) << '\t';
			for (auto sc : bb->succ)
				os << sc << ", ";

			if (bb->reachable == false)
				os << "[UNREACHABLE]";
			os << std::endl;
		}
		if (opts.debug_level > 1)
		{
			os << "; Below are solution of live variables:" << std::endl;
			i = 0ul;
			for (auto bb = funct.basic_blocks.begin();
				bb != funct.basic_blocks.end(); ++bb, ++i)
			{
				os << "; BB #" << i << std::endl;
				os << ";\tuse: ";
				for (auto t : bb->livevar_use)
					os << t << ", ";
				os << std::endl;
				os << ";\tdef: ";
				for (auto t : bb->livevar_def)
					os << t << ", ";
				os << std::endl;
				os << ";\tIN: ";
				for (auto t : bb->livevar_in)
					os << t << ", ";
				os << std::endl;
				os << ";\tOUT: ";
				for (auto t : bb->livevar_out)
					os << t << ", ";
				os << std::endl;
				os << ";\tDOM: ";
				for (auto t : bb->dominators)
					os << t << ", ";
				os << std::endl;

			}
		}
	}
	else if (psymtb == nullptr)
		os << "; This is a CACT builtin function" << std::endl;
	os << '}';
	os << std::endl;
	return os;
}